#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2014, Kevin Carter <kevin.carter@rackspace.com>
#
# Copyright 2014, Rackspace US, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Based on Jimmy Tang's implementation

DOCUMENTATION = """
---
module: keystone
version_added: "1.6.2"
short_description:
    - Manage OpenStack Identity (keystone) users, projects, roles, and
      endpoints.
description:
    - Manage OpenStack Identity (keystone) users, projects, roles, and
      endpoints.
options:
    return_code:
        description:
            - Allow for return Codes other than 0 when executing commands.
            - This is a comma separated list of acceptable return codes.
        default: 0
    login_user:
        description:
            - login username to authenticate to keystone
        required: false
        default: admin
    login_user_domain_name:
        description:
            - The domain login_user belongs to
        required: false
        default: 'Default'
    login_password:
        description:
            - Password of login user
        required: false
        default: 'yes'
    login_project_name:
        description:
            - The project login_user belongs to
        required: false
        default: None
    login_project_domain_name:
        description:
            - The domain login_project belongs to
        required: false
        default: 'Default'
    login_tenant_name:
        description:
            - The tenant login_user belongs to
        required: false
        default: None
    token:
        description:
            - The token to be uses in case the password is not specified
        required: false
        default: None
    endpoint:
        description:
            - The keystone url for authentication
        required: false
    ignore_catalog:
        description:
            - Ignore the service catalog when identifying the endpoint
        required: false
    password:
        description:
            - The password to be assigned to the user
        required: false
        default: None
    user_name:
        description:
            - The name of the user that has to added/removed from OpenStack
        required: false
        default: None
    project_name:
        description:
            - The project name that has be added/removed
        required: false
        default: None
    tenant_name:
        description:
            - The tenant name that has be added/removed
        required: false
        default: None
    role_name:
        description:
            - The name of the role to be assigned or created
        required: false
    service_name:
        description:
            - Name of the service.
        required: false
        default: None
    region_name:
        description:
            - Name of the region.
        required: false
        default: None
    domain_name:
        description:
            - Name of the domain to add a project to.
        required: false
        default: 'Default'
    description:
        description:
            - A description for the project
        required: false
        default: None
    email:
        description:
            - Email address for the user, this is only used in "ensure_user"
        required: false
        default: None
    service_type:
        description:
            - Type of service.
        required: false
        default: None
    endpoint_list:
        description:
            - List of endpoints to add to keystone for a service
        required: false
        default: None
        type: list
    group_name:
        description:
            - A name for the group
        required: False
        default: None
    idp_name:
        description:
            - A name for the identity provider
        required: False
        default: None
    idp_remote_ids:
        description:
            - A URL that identifies the remote identity provider
        required: False
        default: None
    idp_enabled:
        description:
            - Set whether a remote identity provider is enabled
        required: False
        default: True
    sp_name:
        description:
            - A name for the service provider
        required: False
        default: None
    sp_enabled:
        description:
            - Set whether a service provider is enabled
        required: False
        default: True
    sp_url:
        description:
            - URL where the service provider expects to receive SAML assertions
            - eg: http(s)://${SP_HOST}:5000/Shibboleth.sso/SAML2/ECP
        required: False
        default: None
    sp_auth_url:
        description:
            - URL for federated users to request tokens from
            - eg: http(s)://${SP_HOST}:5000/v3/OS-FEDERATION
                  /identity_providers/${IDP_ID}/saml2/auth
        required: False
        default: None
    protocol_name:
        description:
            - A name for the protocol
        required: False
        default: None
    mapping_name:
        description:
            - A name for the mapping
        required: False
        default: None
    mapping_rules:
        description:
            - A dictionary mapping federated users to local groups.
            - see: http://specs.openstack.org/openstack/keystone-specs
                   /api/v3/identity-api-v3-os-federation-ext.html#mappings
        required: False
        default: None
    domain_enabled:
        description:
            - Name for a domain
        required: False
        default: True
    state:
        description:
            - Ensuring the endpoint is either present, absent.
            - It always ensures endpoint is updated to latest url.
        required: False
        default: 'present'
    command:
        description:
            - Indicate desired state of the resource
        choices: ['get_tenant', 'get_project', 'get_user', 'get_role',
                  'ensure_service', 'ensure_endpoint', 'ensure_role',
                  'ensure_user', 'ensure_user_role', 'ensure_tenant',
                  'ensure_project', 'ensure_service_provider',
                  'ensure_group', 'ensure_identity_provider',
                  'ensure_protocol', ensure_mapping',
                  'ensure_group_role']
        required: true
    insecure:
        description:
            - Explicitly allow client to perform "insecure" TLS
        choices:
            - false
            - true
        default: false
requirements: [ python-keystoneclient ]
author: Kevin Carter
"""

EXAMPLES = """
# Create an admin project
- keystone:
    command: "ensure_project"
    project_name: "admin"
    domain_name: "Default"
    description: "Admin project"

# Create a service project
- keystone:
    command: "ensure_project"
    project_name: "service"
    description: "Service project"

# Create an admin user
- keystone:
    command: "ensure_user"
    user_name: "admin"
    project_name: "admin"
    password: "secrete"
    email: "admin@some-domain.com"

# Create an admin role
- keystone:
    command: "ensure_role"
    role_name: "admin"

# Create a user
- keystone:
    command: "ensure_user"
    user_name: "glance"
    project_name: "service"
    password: "secrete"
    domain_name: "Default"
    email: "glance@some-domain.com"

# Add a role to a user
- keystone:
    command: "ensure_user_role"
    user_name: "glance"
    project_name: "service"
    role_name: "admin"

# Add a project role to a group
- keystone:
    command: "ensure_group_role"
    group_name: "fedgroup"
    project_name: "fedproject"
    role_name: "_member_"

# Create a service
- keystone:
    command: "ensure_service"
    service_name: "glance"
    service_type: "image"
    description: "Glance Image Service"

# Create an endpoint
- keystone:
    command: "ensure_endpoint"
    region_name: "RegionOne"
    service_name: "glance"
    service_type: "image"
    endpoint_list:
      - url: "http://127.0.0.1:9292"
        interface: "public"
      - url: "http://127.0.0.1:9292"
        interface: "admin"
      - url: "http://127.0.0.1:9292"
        interface: "internal"

# Get project id
- keystone:
    command: "get_project"
    project_name: "admin"

# Get user id
- keystone:
    command: "get_user"
    user_name: "admin"

# Get role id
- keystone:
    command: "get_role"
    user_name: "admin"

"""

COMMAND_MAP = {
    'get_tenant': {
        'variables': [
            'project_name',
            'tenant_name'
        ]
    },
    'get_project': {
        'variables': [
            'project_name',
            'tenant_name'
        ]
    },
    'get_user': {
        'variables': [
            'user_name'
        ]
    },
    'get_role': {
        'variables': [
            'role_name',
            'project_name',
            'tenant_name',
            'user_name'
        ]
    },
    'ensure_service': {
        'variables': [
            'service_name',
            'service_type',
            'description'
        ]
    },
    'ensure_endpoint': {
        'variables': [
            'region_name',
            'service_name',
            'service_type',
            'endpoint_list',
            'state'
        ]
    },
    'ensure_role': {
        'variables': [
            'role_name'
        ]
    },
    'ensure_user': {
        'variables': [
            'project_name',
            'tenant_name',
            'user_name',
            'password',
            'email',
            'domain_name'
        ]
    },
    'ensure_user_role': {
        'variables': [
            'user_name',
            'project_name',
            'tenant_name',
            'role_name',
            'domain_name'
        ]
    },
    'ensure_group_role': {
        'variables': [
            'group_name',
            'project_name',
            'role_name',
            'domain_name'
        ]
    },
    'ensure_project': {
        'variables': [
            'project_name',
            'tenant_name',
            'description',
            'domain_name'
        ]
    },
    'ensure_tenant': {
        'variables': [
            'project_name',
            'tenant_name',
            'description',
            'domain_name'
        ]
    },
    'ensure_group': {
        'variables': [
            'group_name',
            'domain_name'
        ]
    },
    'ensure_identity_provider': {
        'variables': [
            'idp_name',
            'idp_remote_ids',
            'idp_enabled'
        ]
    },
    'ensure_service_provider': {
        'variables': [
            'sp_name',
            'sp_url',
            'sp_auth_url',
            'sp_enabled'
        ]
    },
    'ensure_protocol': {
        'variables': [
            'protocol_name',
            'idp_name',
            'mapping_name'
        ]
    },
    'ensure_mapping': {
        'variables': [
            'mapping_name',
            'mapping_rules',
        ]
    },
    'ensure_domain': {
        'variables': [
            'domain_name',
            'domain_enabled'
        ]
    }
}

try:
    from keystoneclient import exceptions as kexceptions
    from keystoneclient import client
    from keystoneauth1.identity import v3
    from keystoneauth1 import session

except ImportError:
    keystoneclient_found = False
else:
    keystoneclient_found = True


class ManageKeystone(object):
    def __init__(self, module):
        """Manage Keystone via Ansible."""
        self.state_change = False
        self.keystone = None

        # Load AnsibleModule
        self.module = module

    def command_router(self):
        """Run the command as its provided to the module."""
        command_name = self.module.params['command']
        if command_name not in COMMAND_MAP:
            self.failure(
                error='No Command Found',
                rc=2,
                msg='Command [ %s ] was not found.' % command_name
            )

        action_command = COMMAND_MAP[command_name]
        if hasattr(self, '%s' % command_name):
            action = getattr(self, '%s' % command_name)
            facts = action(variables=action_command['variables'])
            if facts is None:
                self.module.exit_json(changed=self.state_change)
            else:
                self.module.exit_json(
                    changed=self.state_change,
                    ansible_facts=facts
                )
        else:
            self.failure(
                error='Command not in ManageKeystone class',
                rc=2,
                msg='Method [ %s ] was not found.' % command_name
            )

    @staticmethod
    def _facts(facts):
        """Return a dict for our Ansible facts.

        :param facts: ``dict``  Dict with data to return
        """
        return {'keystone_facts': facts}

    def _get_vars(self, variables, required=None):
        """Return a dict of all variables as found within the module.

        :param variables: ``list``  List of all variables that are available to
                                    use within the Keystone Command.
        :param required: ``list``  Name of variables that are required.
        """
        return_dict = {}
        for variable in variables:
            return_dict[variable] = self.module.params.get(variable)
        else:
            if isinstance(required, list):
                for var_name in required:
                    check = return_dict.get(var_name)
                    if check is None:
                        self.failure(
                            error='Missing [ %s ] from Task or found a None'
                                  ' value' % var_name,
                            rc=000,
                            msg='variables %s - available params [ %s ]'
                                % (variables, self.module.params)
                        )
            return return_dict

    def failure(self, error, rc, msg):
        """Return a Failure when running an Ansible command.

        :param error: ``str``  Error that occurred.
        :param rc: ``int``     Return code while executing an Ansible command.
        :param msg: ``str``    Message to report.
        """
        self.module.fail_json(msg=msg, rc=rc, err=error)

    def _authenticate(self):
        """Return a keystone client object."""
        required_vars = ['endpoint']
        variables = [
            'endpoint',
            'ignore_catalog',
            'login_user',
            'login_password',
            'login_project_name',
            'login_tenant_name',
            'login_user_domain_name',
            'login_project_domain_name',
            'token',
            'insecure'
        ]
        variables_dict = self._get_vars(variables, required=required_vars)

        endpoint = variables_dict.pop('endpoint')
        login_user = variables_dict.pop('login_user')
        login_password = variables_dict.pop('login_password')
        login_project_name = (variables_dict.pop('login_project_name', None) or
                              variables_dict.pop('login_tenant_name'))
        user_domain_name = variables_dict.pop('login_user_domain_name',
                                              'Default')
        project_domain_name = variables_dict.pop('login_project_domain_name',
                                                 'Default')
        token = variables_dict.pop('token')
        insecure = variables_dict.pop('insecure')

        if token is None:
            if login_project_name is None:
                self.failure(
                    error='Missing Project Name',
                    rc=2,
                    msg='If you do not specify a token you must use a project'
                        ' name for authentication. Try adding'
                        ' [ login_project_name ] to the task'
                )
            if login_password is None:
                self.failure(
                    error='Missing Password',
                    rc=2,
                    msg='If you do not specify a token you must use a password'
                        ' name for authentication. Try adding'
                        ' [ login_password ] to the task'
                )

        if token:
            self.keystone = client.Client(
                insecure=insecure,
                endpoint=endpoint,
                token=token
            )
        else:
            auth_args = {
                'auth_url': endpoint,
                'username': login_user,
                'user_domain_name': user_domain_name,
                'password': login_password,
                'project_name': login_project_name,
                'project_domain_name': project_domain_name,
            }
            client_args = {
                'insecure': insecure,
            }

            if variables_dict.pop('ignore_catalog'):
                client_args.update(endpoint_override=endpoint)
            auth = v3.Password(**auth_args)
            sess = session.Session(auth=auth, verify=(not insecure))
            self.keystone = client.Client(session=sess,
                                          **client_args)

    def _get_domain_from_vars(self, variables):
        # NOTE(sigmavirus24): Since we don't require domain, this will be None
        # in the dictionary. When we pop it, we can't provide a default
        # because 'domain' exists and is None. In order to use a default
        # value, we need to use `or 'default'` here to make sure we default to
        # the default domain. If we don't do it this way, Keystone throws a
        # 401 Unauthorized which is just plain wrong.
        domain_name = variables.pop('domain_name', None) or 'Default'

        return self._get_domain(name=domain_name)

    def _get_domain(self, name):
        """Return domain information.

        :param str name: Name of the domain.
        """
        for entry in self.keystone.domains.list():
            if entry.name == name:
                return entry
        else:
            return None

    def _get_project(self, name):
        """Return project information.

        Formerly, _get_tenant

        :param name: ``str``  Name of the project.
        """
        for entry in self.keystone.projects.list():
            if entry.name == name:
                return entry
        else:
            return None

    def get_tenant(self, variables):
        return self.get_project(variables)

    def get_project(self, variables):
        """Return a project id.

        This will return `None` if the ``name`` is not found.

        :param variables: ``list``  List of all variables that are available to
                                    use within the Keystone Command.
        """
        self._authenticate()
        variables_dict = self._get_vars(variables)
        project_name = (variables_dict.pop('project_name', None) or
                        variables_dict.pop('tenant_name'))
        project = self._get_project(name=project_name)
        if project is None:
            self.failure(
                error='project [ %s ] was not found.' % project_name,
                rc=2,
                msg='project was not found, does it exist?'
            )

        return self._facts(facts={'id': project.id})

    def ensure_tenant(self, variables):
        return self.ensure_project(variables)

    def ensure_project(self, variables):
        """Create a new project within Keystone if it does not exist.

        Returns the project ID on a successful run.

        :param variables: ``list``  List of all variables that are available to
                                    use within the Keystone Command.
        """
        self._authenticate()
        variables_dict = self._get_vars(variables)
        project_name = (variables_dict.pop('project_name', None) or
                        variables_dict.pop('tenant_name'))
        project_description = variables_dict.pop('description')
        if project_description is None:
            project_description = 'Project %s' % project_name

        domain = self._get_domain_from_vars(variables_dict)
        project = self._get_project(name=project_name)
        if project is None:
            self.state_change = True
            project = self.keystone.projects.create(
                name=project_name,
                description=project_description,
                domain=domain,
                enabled=True
            )

        return self._facts(facts={'id': project.id})

    def _get_user(self, name, domain):
        """Return a user information.

        This will return `None` if the ``name`` is not found.

        :param name: ``str``  Name of the user.
        """
        for entry in self.keystone.users.list(domain=domain):
            if getattr(entry, 'name', None) == name:
                return entry
        else:
            return None

    def get_user(self, variables):
        """Return a project id.

        This will return `None` if the ``name`` is not found.

        :param variables: ``list``  List of all variables that are available to
                                    use within the Keystone Command.
        """
        self._authenticate()
        variables_dict = self._get_vars(variables, required=['user_name'])
        user_name = variables_dict.pop('user_name')
        domain = self._get_domain_from_vars(variables_dict)
        user = self._get_user(name=user_name, domain=domain)
        if user is None:
            self.failure(
                error='user [ %s ] was not found.' % user_name,
                rc=2,
                msg='user was not found, does it exist?'
            )

        return self._facts(facts={'id': user.id})

    def ensure_user(self, variables):
        """Create a new user within Keystone if it does not exist.

        Returns the user ID on a successful run.

        :param variables: ``list``  List of all variables that are available to
                                    use within the Keystone Command.
        """
        self._authenticate()
        required_vars = ['user_name', 'password']
        variables_dict = self._get_vars(variables, required=required_vars)
        project_name = (variables_dict.pop('project_name', None) or
                        variables_dict.pop('tenant_name'))
        password = variables_dict.pop('password')
        user_name = variables_dict.pop('user_name')
        email = variables_dict.pop('email')

        domain = self._get_domain_from_vars(variables_dict)
        project = self._get_project(name=project_name)
        if project is None and project_name is not None:
            self.failure(
                error='project [ %s ] was not found.' % project_name,
                rc=2,
                msg='project was not found, does it exist?'
            )

        user = self._get_user(name=user_name, domain=domain)
        if user is None:
            self.state_change = True
            user = self.keystone.users.create(
                name=user_name,
                password=password,
                email=email,
                domain=domain,
                default_project=project
            )

        return self._facts(facts={'id': user.id})

    def _get_role(self, name, domain):
        """Return a role by name.

        This will return `None` if the ``name`` is not found.

        :param name: ``str``  Name of the role.
        :param domain: ``str`` ID of the domain
        """
        for entry in self.keystone.roles.list(domain=domain):
            if entry.name == name:
                return entry
        else:
            return None

    def _get_group(self, name, domain='Default'):
        """Return a group by name.

        This will return `None` if the ``name`` is not found.

        :param name: ``str``  Name of the role.
        """
        for entry in self.keystone.groups.list(domain=domain):
            if domain is None:
                if entry.name == name:
                    return entry
            else:
                if entry.name == name and entry.domain_id == domain.id:
                    return entry
        else:
            return None

    def get_role(self, variables):
        """Return a role by name.

        This will return `None` if the ``name`` is not found.

        :param variables: ``list``  List of all variables that are available to
                                    use within the Keystone Command.
        """
        self._authenticate()
        variables_dict = self._get_vars(variables, required=['role_name'])
        role_name = variables_dict.pop('role_name')
        domain = self._get_domain_from_vars(variables_dict)
        role_data = self._get_role(name=role_name, domain=domain)
        if role_data is None:
            self.failure(
                error='role [ %s ] was not found.' % role_name,
                rc=2,
                msg='role was not found, does it exist?'
            )

        return self._facts(facts={'id': role_data.id})

    def _get_role_data(self, user_name, project_name, role_name, group_name,
                       domain):
        if user_name is not None:
            user = self._get_user(name=user_name, domain=domain)
            if user is None:
                self.failure(
                    error='user [ %s ] was not found.' % user_name,
                    rc=2,
                    msg='User was not found, does it exist?'
                )
        else:
            user = None

        project = self._get_project(name=project_name)
        if project is None and project_name is not None:
            self.failure(
                error='project [ %s ] was not found.' % project_name,
                rc=2,
                msg='project was not found, does it exist?'
            )

        role = self._get_role(name=role_name, domain=domain)
        if role is None:
            self.failure(
                error='role [ %s ] was not found.' % role_name,
                rc=2,
                msg='role was not found, does it exist?'
            )

        if group_name is not None:
            group = self._get_group(name=group_name, domain=domain)
            if group is None:
                self.failure(
                    error='group [ %s ] was not found.' % group_name,
                    rc=2,
                    msg='group was not found, does it exist?'
                )
        else:
            group = None

        return user, project, role, group

    def ensure_role(self, variables):
        """Create a new role within Keystone if it does not exist.

        Returns the user ID on a successful run.

        :param variables: ``list``  List of all variables that are available to
                                    use within the Keystone Command.
        """
        self._authenticate()
        variables_dict = self._get_vars(variables, required=['role_name'])
        domain = self._get_domain_from_vars(variables_dict)
        role_name = variables_dict.pop('role_name')

        role = self._get_role(name=role_name, domain=domain)
        if role is None:
            self.state_change = True
            role = self.keystone.roles.create(role_name)

        return self._facts(facts={'id': role.id})

    def _get_user_roles(self, name, user, project, domain):
        role_list = self.keystone.roles.list(
            user=user,
            project=project,
            domain=domain
        )
        for entry in role_list:
            if entry.name == name:
                return entry
        else:
            return None

    def _get_group_roles(self, name, group, project, domain):
        group_list = self.keystone.roles.list(
            group=group,
            project=project,
            domain=domain
        )
        for entry in group_list:
            if entry.name == name:
                return entry
        else:
            return None

    def ensure_user_role(self, variables):
        self._authenticate()
        required_vars = ['user_name', 'role_name']
        variables_dict = self._get_vars(variables, required=required_vars)
        domain = self._get_domain_from_vars(variables_dict)
        user_name = variables_dict.pop('user_name')
        # NOTE(sigmavirus24): Try to get the project_name, but
        # don't error out on it. This will change when the playbooks are
        # updated to use project_name instead of tenant_name
        project_name = (variables_dict.pop('project_name', None) or
                        variables_dict.pop('tenant_name'))
        role_name = variables_dict.pop('role_name')

        if project_name is not None:
            domain = None

        user, project, role, group = self._get_role_data(
            user_name=user_name, project_name=project_name,
            role_name=role_name, group_name=None, domain=domain
        )

        user_role = self._get_user_roles(
            name=role_name, user=user, project=project, domain=domain
        )

        if user_role is None:
            self.state_change = True
            self.keystone.roles.grant(
                user=user, role=role, project=project, domain=domain
            )
            user_role = self._get_user_roles(
                name=role_name, user=user, project=project, domain=domain
            )

        return self._facts(facts={'id': user_role.id})

    def ensure_group_role(self, variables):
        self._authenticate()
        required_vars = ['group_name', 'project_name', 'role_name']
        variables_dict = self._get_vars(variables, required=required_vars)
        domain = self._get_domain_from_vars(variables_dict)
        group_name = variables_dict.pop('group_name')
        project_name = variables_dict.pop('project_name')
        role_name = variables_dict.pop('role_name')

        if project_name is not None:
            domain = None

        user, project, role, group = self._get_role_data(
            group_name=group_name, project_name=project_name,
            role_name=role_name, user_name=None, domain=domain
        )

        group_role = self._get_group_roles(
            name=role_name, group=group, project=project, domain=domain
        )

        if group_role is None:
            self.state_change = True
            self.keystone.roles.grant(
                group=group, role=role, project=project, domain=domain
            )
            group_role = self._get_group_roles(
                name=role_name,
                group=group,
                project=project,
                domain=domain
            )

        return self._facts(facts={'id': group_role.id})

    def ensure_group(self, variables):
        """Create a new group within Keystone if it does not exist.

        Returns the group ID on a successful run.

        :param variables: ``list``  List of all variables that are available to
                                    use within the Keystone Command.
        """

        self._authenticate()
        required_vars = ['group_name', 'domain_name']
        variables_dict = self._get_vars(variables, required=required_vars)
        group_name = variables_dict.pop('group_name')

        domain = self._get_domain_from_vars(variables_dict)

        group = self._get_group(
            name=group_name, domain=domain
        )

        if group is None:
            self.state_change = True
            group = self.keystone.groups.create(
                name=group_name, domain=domain
            )

        return self._facts(facts={'id': group.id})

    def _get_service(self, name, srv_type=None):
        for entry in self.keystone.services.list():
            if srv_type is not None:
                if entry.type == srv_type and name == entry.name:
                    return entry
            elif entry.name == name:
                return entry
        else:
            return None

    def ensure_service(self, variables):
        """Create a new service within Keystone if it does not exist.

        Returns the service ID on a successful run.

        :param variables: ``list``  List of all variables that are available to
                                    use within the Keystone Command.
        """
        self._authenticate()
        required_vars = ['service_name', 'service_type']
        variables_dict = self._get_vars(variables, required=required_vars)

        service_name = variables_dict.pop('service_name')
        description = variables_dict.pop('description')
        service_type = variables_dict.pop('service_type')

        service = self._get_service(name=service_name, srv_type=service_type)
        if service is None or service.type != service_type:
            self.state_change = True
            service = self.keystone.services.create(
                name=service_name,
                type=service_type,
                description=description
            )

        return self._facts(facts={'id': service.id})

    def _get_endpoint_by_details(self, region, service_id, interface):
        """ Getting endpoints per complete definition

        Returns the endpoint details for an endpoint matching
        region, service id and interface.

        :param interface: ``str`` 'public', 'admin' or 'internal' network
                                  interface
        :param service_id: service to which the endpoint belongs
        :param region: geographic location of the endpoint

        """
        for entry in self.keystone.endpoints.list():
            check = [
                entry.region == region,
                entry.service_id == service_id,
                entry.interface == interface
            ]
            if all(check):
                return entry
        else:
            return None

    def _get_endpoint(self, region, url, interface):
        """ Getting endpoints per URL
        Returns the endpoint details for an endpoint matching
        URL, region and interface.
        This interface should be deprecated in next release.
        """
        for entry in self.keystone.endpoints.list():
            check = [
                entry.region == region,
                entry.url == url,
                entry.interface == interface
            ]
            if all(check):
                return entry
        else:
            return None

    def ensure_endpoint(self, variables):
        """Ensures the deletion/modification/addition of endpoints
        within Keystone.

        Returns the endpoint ID on a successful run.

        :param variables: ``list``  List of all variables that are available to
                                    use within the Keystone Command.
        """
        self._authenticate()
        required_vars = [
            'region_name',
            'service_name',
            'service_type',
            'endpoint_list'
        ]
        variables_dict = self._get_vars(variables, required=required_vars)

        service_name = variables_dict.pop('service_name')
        service_type = variables_dict.pop('service_type')
        region = variables_dict.pop('region_name')
        endpoint_list = variables_dict.pop('endpoint_list')
        state = variables_dict.pop('state')

        service = self._get_service(name=service_name, srv_type=service_type)
        if service is None:
            self.failure(
                error='service [ %s ] was not found.' % service_name,
                rc=2,
                msg='Service was not found, does it exist?'
            )

        endpoints = {}
        for endpoint_dict in endpoint_list:
            url = endpoint_dict.pop('url')
            interface = endpoint_dict.pop('interface')
            endpoint = self._get_endpoint(
                region=region,
                url=url,
                interface=interface
            )
            if state == 'present':
                ''' Creating an endpoint for this url
                    (if it does not exist) or updating
                    an existing endpoint that matches
                    the service type, name, interface
                    and region.
                '''
                similar_endpoint = self._get_endpoint_by_details(
                    region=region,
                    service_id=service.id,
                    interface=interface
                )
                if similar_endpoint is not None:
                    if similar_endpoint.url != url:
                        self.state_change = True
                        endpoint = self.keystone.endpoints.update(
                            endpoint=similar_endpoint,
                            url=url
                        )
                elif endpoint is None:
                    self.state_change = True
                    endpoint = self.keystone.endpoints.create(
                        region=region,
                        service=service,
                        url=url,
                        interface=interface
                    )
            # The update state is deprecated and should be removed in Q
            elif state == 'update':
                ''' Checking if there is a similar endpoint with a
                    different url. Update it if there is one, create
                    if there is none. Update is deprecated and will
                    be removed in Q. "Present" achieves the same
                    result.
                '''
                similar_endpoint = self._get_endpoint_by_details(
                    region=region,
                    service_id=service.id,
                    interface=interface
                )
                if similar_endpoint is None:
                    self.state_change = True
                    endpoint = self.keystone.endpoints.create(
                        region=region,
                        service=service,
                        url=url,
                        interface=interface
                    )
                elif similar_endpoint.url != url:
                    self.state_change = True
                    endpoint = self.keystone.endpoints.update(
                        endpoint=similar_endpoint,
                        url=url
                    )
            elif state == 'absent':
                if endpoint is not None:
                    self.state_change = True
                    result = self.keystone.endpoints.delete(endpoint.id)
                    if result[0].status_code != 204:
                        module.fail()

        if state != 'absent':
            endpoints[interface] = endpoint
            return self._facts({'%sid' % interface: endpoint.id
                                for interface, endpoint in endpoints.items()})
        else:
            return self._facts({})

    def _ensure_generic(self, manager, required_vars, variables):
        """Try and create a new 'thing' in keystone.

        Thing type is determined by the manager passed in.

        :param: manager - openstack object manager eg self.keystone.groups
        :param: required_vars - dictionary:
                ansible module argument name : manager argument name
                eg {'group_name': 'name'}

        :returns: Facts dictionary with things =
            <list of things converted to dict>

        TODO: make this handle updates as well as creates
        TODO (maybe, if we decide to use this module long term):
            migrate other ensures to use this
        """

        # Get values for variables
        variables_dict = self._get_vars(variables,
                                        required=required_vars.keys())

        # Translate ansible module argument names to manager expected names
        args_dict = {required_vars[k]: v for k, v in variables_dict.items()}

        try:
            manager.create(**args_dict)
            self.state_change = True
        except kexceptions.Conflict:
            self.state_change = False

        try:
            return self._facts(facts={
                manager.collection_key:
                    [x.to_dict() for x in manager.list()]
            })
        except TypeError:
            # some managers require arguments to their list functions :/
            # return no facts in this case.
            return self._facts(facts={})

    def ensure_identity_provider(self, variables):
        self._authenticate()
        return self._ensure_generic(
            manager=self.keystone.federation.identity_providers,
            required_vars={'idp_name': 'id',
                           'idp_remote_ids': 'remote_ids',
                           'idp_enabled': 'enabled'},
            variables=variables
        )

    def ensure_service_provider(self, variables):
        self._authenticate()
        return self._ensure_generic(
            manager=self.keystone.federation.service_providers,
            required_vars={'sp_name': 'id',
                           'sp_auth_url': 'auth_url',
                           'sp_url': 'sp_url',
                           'sp_enabled': 'enabled'},
            variables=variables
        )

    def ensure_protocol(self, variables):
        """Facts not returned

        This is because you can't list protocols without
        specifying an identity provider
        """

        self._authenticate()
        return self._ensure_generic(
            manager=self.keystone.federation.protocols,
            required_vars={'protocol_name': 'protocol_id',
                           'idp_name': 'identity_provider',
                           'mapping_name': 'mapping'},
            variables=variables
        )

    def ensure_mapping(self, variables):
        self._authenticate()
        return self._ensure_generic(
            manager=self.keystone.federation.mappings,
            required_vars={'mapping_name': 'mapping_id',
                           'mapping_rules': 'rules'},
            variables=variables
        )

    def ensure_domain(self, variables):
        self._authenticate()
        return self._ensure_generic(
            manager=self.keystone.domains,
            required_vars={'domain_name': 'name',
                           'domain_enabled': 'enabled'},
            variables=variables
        )


# TODO(evrardjp): Deprecate state=update in Q.
def main():
    module = AnsibleModule(
        argument_spec=dict(
            login_user=dict(
                required=False
            ),
            login_user_domain_name=dict(
                required=False,
                default='Default'
            ),
            login_password=dict(
                required=False,
                no_log=True
            ),
            login_tenant_name=dict(
                required=False
            ),
            login_project_name=dict(
                required=False
            ),
            login_project_domain_name=dict(
                required=False,
                default='Default'
            ),
            token=dict(
                required=False
            ),
            password=dict(
                required=False,
                no_log=True
            ),
            endpoint=dict(
                required=True,
            ),
            ignore_catalog=dict(
                required=False
            ),
            user_name=dict(
                required=False
            ),
            tenant_name=dict(
                required=False
            ),
            project_name=dict(
                required=False
            ),
            domain_name=dict(
                required=False
            ),
            role_name=dict(
                required=False
            ),
            service_name=dict(
                required=False
            ),
            region_name=dict(
                required=False
            ),
            description=dict(
                required=False
            ),
            email=dict(
                required=False
            ),
            service_type=dict(
                required=False
            ),
            endpoint_list=dict(
                required=False,
                type='list'
            ),
            command=dict(
                required=True,
                choices=COMMAND_MAP.keys()
            ),
            insecure=dict(
                default=False,
                required=False,
                type='bool'
            ),
            return_code=dict(
                type='str',
                default='0'
            ),
            group_name=dict(
                type='str',
                required=False
            ),
            idp_remote_ids=dict(
                type='list',
                required=False,
            ),
            idp_name=dict(
                type='str',
                required=False,
            ),
            idp_enabled=dict(
                type='bool',
                default=True,
                required=False,
            ),
            sp_name=dict(
                type='str',
                required=False,
            ),
            sp_auth_url=dict(
                type='str',
                required=False,
            ),
            sp_url=dict(
                type='str',
                required=False,
            ),
            sp_enabled=dict(
                type='bool',
                default=True,
                required=False,
            ),
            protocol_name=dict(
                type='str',
                required=False,
            ),
            mapping_name=dict(
                type='str',
                required=False,
            ),
            mapping_rules=dict(
                type='list',
                required=False,
            ),
            domain_enabled=dict(
                type='bool',
                required=False,
                default=True
            ),
            state=dict(
                choices=['present', 'absent', 'update'],
                required=False,
                default='present'
            )
        ),
        supports_check_mode=False,
        mutually_exclusive=[
            ['token', 'login_user'],
            ['token', 'login_password'],
            ['token', 'login_tenant_name']
        ]
    )

    km = ManageKeystone(module=module)
    if not keystoneclient_found:
        km.failure(
            error='python-keystoneclient is missing',
            rc=2,
            msg='keystone client was not importable, is it installed?'
        )

    return_code = module.params.get('return_code', '').split(',')
    module.params['return_code'] = return_code
    km.command_router()


# import module snippets
from ansible.module_utils.basic import *  # NOQA
if __name__ == '__main__':
    main()
